<?php
/**
 * controller.class
 * 
 * MVC part. Controller class
 * 
 * @version:	0.2b
 * @package:	core
 * @author:	alex_ez
 * @created:	11 nov 2008
 *
 * @copyright:	(c) 2009 alex <qfox@ya.ru> yaroshevich aka alex_ez. All rights reserved.
 */

if( defined( '__CONTROLLER.CLASS_PHP' ) ) return;
define( '__CONTROLLER.CLASS_PHP', '0.2b' );

abstract class basic_controller
{
	private $viewdata = null; // some data
	
	static private $__is = null; // instances
	static private $__page = null;
	
	protected $frame = '!main.tpl';
	protected $templatens = null;
	protected $crumbs = null;
	
	protected $visitor = null;
	protected $visitorRoles = null;
	
	final public function __construct( )
	{
		$this ->visitor = visitor::__getMe( );
		$this ->visitorRoles = $this ->visitor ->roles;
		
		$this ->config = config::__getMe( );
		
		$this ->hooks = new Hooks( $this );
		$this ->hooks ->fire( 'load' );
		
		$this ->crumbs = array( '/' => _::pick( $this ->config ->home, 'Home' ) );
		if( is_null( self::$__page ) ) self::$__page = str_replace( 'Controller', '', get_class( $this ) );
		
		$ns = str_replace( 'Controller', '', get_class( $this ) );
		if( strtolower(substr( $ns, -5 )) == 'admin' ) { $ns = 'admin/'.substr( $ns, 0, -5 ); }
		$this ->templatens = $ns;
		
		$this ->onstart( );
		$this ->hooks ->fire( 'initialize' );
	}

	/**
	 * Start event method
	 * 
	 * @deprecated
	 */
	protected function onstart( ) { /*throw new Exception( "You must describe onstart event for page controllerrrr." );*/ }

	/**
	 * Gets instance of needed controller
	 * 
	 * @param string $c controllers class name
	 * @return controller
	 */
	static public function &__getInstanceOf( $c = null )
	{
		if( is_null( $c ) ) return;
		if( is_null( self::$__is ) ) self::$__is = array( );
		
		if( isset( self::$__is[ $c ] ) )
			return self::$__is[ $c ];
		
		$classExists = !is_null( get_class_methods( $c ) );
		if( !$classExists ) { $null = null; return $null; }
		
		self::$__is[ $c ] = new $c( );
		// self::$__is[ $c ] ->start( ); // overpower ;-(
		
		if( self::$__is[ $c ] instanceof controller )
			return self::$__is[ $c ];
		
		throw new Exception( "`$c' class is not extended by `controller' class." );
		
		unset( self::$__is[ $c ] );
		//self::$__is[ $c ]
		return;
	}

	/**
	 * Modern __getInstanceOf
	 *
	 */
	static public function &getInstanceOf( $c = null )
	{
		$c = self::parseAction( $c.'::index' );
		return self::__getInstanceOf( $c['class'] );
	}
	
	/**
	 * Finds page by url
	 *
	 * @param string $url
	 * @return array|null page if founded, null otherwise
	 */
	static public function findPage( $url )
	{
		$pages = self::getPages( );
		if( is_integer( $url ) )
			return isset( $pages[ (int)$url ] ) ? $pages[ (int)$url ] : null;
		
		foreach( $pages as $v )
		if( $v['state'] !== 'off' )
		{
			$m = array( );
			$_r = ( $v['url']{0} == '@' )
				? ( preg_match( str_replace( '/$', '/?$', $v['url'] ), $url, $m ) )
				: ( $v['url'] == $url || rtrim($v['url'],'/') == $url );
			
			// normalizing path via redirect 301
			self::normalizePath( $url );
			
			// searching
			if( !$_r ) continue;
			
			// pages with dev state showed for developers (overpower)
			if( $v['state'] === 'dev' && !(request::__getMe( ) ->isDeveloper( )) ) continue;
			
			// parse action and fetch params of founded page and return them
			$_action = self::parseAction( $v['action'] );
			$v[ 'class' ] = $_action['class'];
			$v[ 'method' ] = $_action['method'].'Page';
			
			if( !empty( $m ) ) array_shift( $m );
			$v[ 'params' ] = $m;
			
			return $v;
		}

		// null if page not found
		return null;
	}

	/**
	 * Finds ajax action by url
	 *
	 * @param string $url request::$path by default
	 * @return array|null ajax action if founded, null otherwise
	 */
	static public function findAjax( $url = null )
	{
		$request = request::__getMe( );
		
		$path = $request ->path;
		if( !is_null( $url ) )
		{
			$path = array_filter( explode( '/', $url ), create_function( '$a', 'return !!$a;' ) );
		}
		
		// remove ajax
		array_shift( $path );
		
		// determiate namespace
		$_ns = 'public';
		$cp = array( 'public' => '', 'admin' => 'Admin' );
		if( $path[0] == 'admin' ) $_ns = array_shift( $path );
		$cp = $cp[$_ns];
		
		// class methods with higher priority than methods
		$c = $path;
		$m = array( );
		
		$v = array( );
		while( count( $c ) > 1 ) {
			// try to be deeper
			array_unshift( $m, array_pop( $c ) );
			$v[ 'class' ] = join( '_', $c ).$cp.'Controller';
			$v[ 'method' ] = 'ajax'.ucfirst( join( '_', $m ) );
			$v[ 'action' ] = self::renderAction( $v['class'], $v['method'] ); //'ajax::'.($_ns!='public'?"{$_ns}::":'')."{$c}::".strtolower( join( '_', $path ) );
			if( method_exists( $v['class'], $v['method'] ) ) break;
		}
		if( !method_exists( $v['class'], $v['method'] ) ) { fb::error( $v, 'not found ;(' ); return null; }
		
		self::normalizePath( $request ->raw_url );
		
		$v[ 'params' ] = $request ->data;
		
		return $v;
	}

	/**
	 * Redirect user to normalized url if not
	 * 
	 * @param string $url Current url
	 */
	static public function normalizePath( $url )
	{
		if( mb_strlen( $url ) <= 2 ) return;
		if( !strpos( ' .php .xml', substr( $url, -4, 4 ) ) && substr( $url, -1, 1 ) != '/' )
			response::__getMe( ) ->moved( $url.'/'.(strlen($_SERVER['QUERY_STRING'])?('?'.$_SERVER['QUERY_STRING']):'') );
		if( strpos( ' .php/ .xml/', substr( $url, -5, 5 ) ) )
			response::__getMe( ) ->moved( rtrim($url,'/').(strlen($_SERVER['QUERY_STRING'])?('?'.$_SERVER['QUERY_STRING']):'') );
	}

	/**
	 * Action parser
	 *
	 * @param string $action
	 * @return array
	 */
	static public function parseAction( $action )
	{
		// todo: rewrite
		preg_match( '@^(?:(?P<special>ajax)(?:::)|)(?:(?P<ns>admin)(?:::)|)(?:(?P<controller>[a-z]{1}[a-z\_0-9]+?)(?:controller|page)?|)::(?P<method>[a-z]{1}[a-z\_0-9]+)(?:page|block)?$@iu', $action, $n );
		if( $n['controller'] === '' ) $n['controller'] = self::getPageController();
		if( $n['special'] ) $n['method'] = $n['special'].strtoupper($n['method'][0]).substr($n['method'],1);
		
		$controllerSuffix = ($n['ns']=='admin')?'Admin':'';
		$controllerSuffix .= 'Controller';
		$r = array( 'class' => $n['controller'].$controllerSuffix, 'method' => $n['method'] );
		
		return $r;
	}

	/**
	 * Action render
	 *
	 * @param string $controller
	 * @param string $method
	 * @return string
	 */
	static public function renderAction( $controller, $method = '' )
	{
		$action = '';
		if( 0 === stripos( $method, 'ajax' ) ) { $action .= 'ajax::'; $method = strtolower($method[4]).substr( $method, 5 ); }
		if( false !== stripos( $controller, 'AdminController' ) ) $action .= 'admin::';
		// todo: rewrite
		$action .= preg_replace( '@(admin)?controller$@i', '', $controller ).'::';
		$action .= preg_replace( '@(page|block)$@i', '', $method );
		
		$action = rtrim( $action, ':' );
		
		return $action;
	}
	
	/**
	 * Gets main page controller classname
	 *
	 * @return string Owner controller classname
	 */
	static public function getPageController( )
	{
		return self::$__page;
	}

	/**
	 * Gets all possible page urls
	 *
	 * @return array Array with page arrays
	 */
	static public function getPages( )
	{
		$cache = cache::__getMe( );
		$cache ->enable( 'pages' );
		if( $cache ->is( __CLASS__, __METHOD__, null, '1M' ) )
		{
			$r = $cache ->getv( );
			$cache ->disable( 'pages' );
			return $r;
		}
		
		$pages = config::__getMe( ) ->pages;
		// base by default
		$base = self::getBasePages( );
		$public = self::getPublicPages( );
		$r = $pages == 'both' ? array_merge( $base, $public ) : ( $pages == 'public' ? $public : $base );
		
		$r = array_merge( $r, self::getAdminPages( ) );
		usort( $r, create_function( '$a,$b', '$a=$a["priority"];$b=$b["priority"]; if($a==$b)return 0; return $a<$b?-1:1;' ) );
		
		$cache ->setv( $r );
		$cache ->disable( 'pages' );
		
		return $r;
	}

	/**
	 * Gets page urls from database
	 *
	 * @return array Array with page arrays
	 */
	static public function getBasePages( )
	{
		$cache = cache::__getMe( );
		if( $cache ->is( __CLASS__, __METHOD__, null, '1M' ) )
		{
			return $cache ->get( __CLASS__, __METHOD__, null );
		}
		
		$db = db::__getMe( );
		$_r = $db ->_list( "SELECT * FROM core_pages ORDER BY priority ASC, id ASC;" );
		
		$cache ->set( __CLASS__, __METHOD__, null, $_r );
		
		return $_r;
	}

	/**
	 * Gets public pages urls in controllers
	 *
	 * @return array Array with page arrays
	 */
	static public function getPublicPages( )
	{
		$cache = cache::__getMe( );
		if( $cache ->is( __CLASS__, __METHOD__, null, '1M' ) )
			return $cache ->getv( );
		
		$_r = array( );
		
		$exts_globs = glob( __controllers.'*.php' );
		$core_globs = glob( __core_controllers.'*.php' );
		$globs = array_merge( $exts_globs, $core_globs );
		
		$classes = array( );
		$_public = strtolower(config::__getMe( ) ->public);
		//fb::log( $_public, '_public' );
		foreach( $globs as $c )
		{
			$className = pathinfo( $c, PATHINFO_FILENAME );
			// todo: rewrite this block to parser and analizer with wildcards or regexps
			if( $_public !== '*' && (strpos( $_public, strtolower($className) ) === false) ) continue;
			
			if( array_key_exists( $className, $classes ) ) continue;
			$classes[$className] = 0;
			$priority = strtolower($className) == 'index' ? 70 : 50;
			$methods = get_class_methods( $className.'Controller' );
			if( is_array( $methods ) )
			foreach( $methods as $m )
			{
			if( preg_match( '@^show(?P<method>[a-z_]+)Page$@i', $m, $_m ) !== 1 ) continue;
			$classes[$className] ++;
			
			$methodName = $_m['method'];
			$action = $className.'::'.strtolower( $methodName[0] ).substr( $methodName, 1 );
			$_page = array( 'state' => 'on', 'options' => '', 'priority' => $priority + ((strtolower($methodName) == 'index')?-50:10) );
			$_page['action'] = $action;
			$_page['url'] = strtolower( str_replace( '//', '/', '/'.trim( str_replace( array( '::', '_' ), '/', str_replace( array( 'index', '::index' ), '::', $action ) ), ' /').'/' ) );
			$_page['class'] = str_replace( realpath( root ).'/', '', $c );
			$_r[] = $_page;
			}
		}
		usort( $_r, create_function( '$a,$b', '$a=$a["priority"];$b=$b["priority"]; if($a==$b)return 0; return $a<$b?-1:1;' ) );
		//fb::log( $_r );
 		
		$cache ->set( __CLASS__, __METHOD__, null, $_r );
		
		return $_r;
	}

	/**
	 * Gets admin pages urls in controllers
	 *
	 * @return array Array with page arrays
	 */
	static public function getAdminPages( )
	{
		$cache = cache::__getMe( );
		if( $cache ->is( __CLASS__, __METHOD__, null, '1M' ) )
			return $cache ->get( __CLASS__, __METHOD__, null );
		
		$_r = array( );
		
		$exts_globs = glob( __controllers.'admin/*.php' );
		$core_globs = glob( __core_controllers.'admin/*.php' );
		$globs = array_merge( $exts_globs, $core_globs );
		
		$classes = array( );
		foreach( $globs as $c )
		{
			$className = pathinfo( $c, PATHINFO_FILENAME );
			if( array_key_exists( $className, $classes ) ) continue;
			$classes[$className] = 0;
			$priority = strtolower($className) == 'index' ? 120 : 100;
			$methods = get_class_methods( $className.'AdminController' );
			if( is_array( $methods ) )
			foreach( $methods as $m )
			{
			if( preg_match( '@^show(?P<method>[a-z_]+)Page$@i', $m, $_m ) !== 1 ) continue;
			$classes[$className] ++;
			
			$methodName = $_m['method'];
			$action = $className.'::'.strtolower( $methodName[0] ).substr( $methodName, 1 );
			$_page = array( 'state' => 'on', 'options' => '', 'priority' => $priority + ((strtolower($methodName) == 'index')?-50:10) );
			$_page['action'] = 'admin::'.$action;
			$_page['url'] = '/admin'.strtolower( str_replace( '//', '/', '/'.trim( str_replace( array( '::', '_' ), '/', str_replace( array( 'index', '::index' ), '::', $action ) ), ' /').'/' ) );
			$_page['class'] = str_replace( realpath( root ).'/', '', $c );
			$_r[] = $_page;
			}
		}
		usort( $_r, create_function( '$a,$b', '$a=$a["priority"];$b=$b["priority"]; if($a==$b)return 0; return $a<$b?-1:1;' ) );
		//fb::log( $_r, 'admin pages' );
 		
		$cache ->set( __CLASS__, __METHOD__, null, $_r );
		
		return $_r;
	}
	
	/**
	 * Gets public controllers
	 *
	 * @return array Array with public controllers names
	 */
	static public function getPublicControllers( )
	{
		$globs = array_merge( glob( __controllers.'*.php' ), glob( __core_controllers.'*.php' ) );
		$classes = array( ); foreach( $globs as $c ) $classes[] = pathinfo( $c, PATHINFO_FILENAME );
		
		array_unshift( $classes, 'index' ); $classes = array_unique( $classes );
		
		return $classes;
	}

	/**
	 * Gets admin controllers
	 *
	 * @return array Array with admin controllers names
	 */
	static public function getAdminControllers( )
	{
		$globs = array_merge( glob( __controllers.'admin/*.php' ), glob( __core_controllers.'admin/*.php' ) );
		$classes = array( ); foreach( $globs as $c ) $classes[] = pathinfo( $c, PATHINFO_FILENAME );
		return $classes;
	}



	/**
	 * Unpacker for options
	 *
	 * @param string $o String with data in serialize or json or urlquery
	 * @return array|null
	 */
	static protected function unpackOptions( $o )
	{
		if( ($_r = @unserialize( $o )) !== false )	return $_r;
		if( is_array( $_r = @json::decode( $o, 1 ) ) )	return $_r;
		
		parse_str( $o, $_r );
		return is_array( $_r ) ? $_r : null;
	}

	/**
	 * Packer for options
	 * 
	 * @param array $o
	 * @return string json encoded string
	 */
	static protected function packOptions( $o )
	{
		return json::encode( $o );
	}
	
	
	
	public function __set( $k, $v )
	{
		if( !$k || !is_string( $k ) ) return null;
		
		if( strpos( $k, '__' ) !== false )
		{
			$ak = explode( '__', $k );
			if( count( $ak ) != 2 ) return;
			$k = $ak[0];
			$v = array( $ak[1] => $v );
		}
		
		switch( $k )
		{
		case "meta":
		case "view":
			if( is_string( $v ) ) $v = self::unpackOptions( $v );
			if( !is_array( $v ) ) break;
			foreach( $v as $vk => $vv )
			$this ->viewdata[ $k ][ $vk ] = $vv;
		break;
		case "title":
			if( !is_string( $v ) ) $v = join( ' — ', $v );
			$this ->viewdata['title'] = $v;
			$this ->viewdata['meta']['title'] = $v;
		break;
		default:
		$this ->$k = $v;
		}
	}
	
	public function __get( $k )
	{
		return isset( $this ->viewdata[ $k ] ) ? $this ->viewdata[$k] : ( isset( $this ->$k ) ? $this ->$k : null );
	}
	
	public function __call( $_m, $p )
	{
		if( isset( $p[1] ) && !is_array( $p[1] ) && strlen($p[1]) )
			$p[1] = self::unpackOptions( $p[1] );
		if( !isset( $p[1] ) ) $p[1] = null;
		
		$view = view::__getMe( );
		$__m = str_replace( array( 'Page', 'Block' ), '', $_m );
		$_r = null; $ob = '';
		
		$type = (strpos( $_m, 'Page' ) !== false) ? 1 : 2;
		$template = isset( $p[1]['template'] ) ? $p[1]['template'] : $__m.'.tpl';
		$templatens = isset( $p[1]['templatens'] ) ? $p[1]['templatens'] : $this ->templatens;
		
		$this ->hooks ->fire( 'call', array( 'type' => $type, 'method' => $__m ) );
		
		switch( $type )
		{
		case 1:
		case 'page':
		
		$m = 'show'.ucfirst($__m).'Page';
		if( method_exists( $this, $m ) )
		{
			ob_start( );
			$__r = call_user_func_array( array( $this, $m ), $p );
			$ob = ob_get_contents(); ob_end_clean();
			
			if( is_array( $__r ) )
			{
				// metas
				if( !isset( $this ->meta['title'] ) && isset( $__r['title'] ) ) $this ->title = $__r['title'];
				// template
				$template = isset( $__r['template'] ) ? $__r['template'] : $template;
				$templatens = isset( $__r['templatens'] ) ? $__r['templatens'] : $templatens;
				$__r['__url'] = $this ->getCurrentUrl( );
				// if( strtolower(substr( $__c, -5 )) == 'admin' ) { $__c = 'admin/'.substr( $__c, 0 ,-5 ); }
				
				define( 'QFOX_PAGE_DRAWING', 1 );
				$_r = $view ->display( array(
					str_replace('_','/',$templatens).'/'.$template,
					$templatens.'@'.$template,
					$template 
				), $__r );
			} else
				$_r = $__r;
			
			$_r = $ob.$_r;
			
			$this ->hooks ->fire( 'display', array( 'type' => $type, 'method' => $__m, 'result' => &$_r ) );
			
			$frame = (request::__getMe( ) ->g_noframe)
				? false
				: ( (is_array($__r)&&isset( $__r['frame'] ))
					? $__r['frame']
					: ( isset( $p[1]['frame'] )
						? $p[1]['frame']
						: $this ->frame ) );
			
			if( $frame )
			{
			$data = $this ->viewdata;
			$data['view']['controller'] = get_class( $this );
			$data['view']['method'] = $__m;
			$data['view']['action'] = self::renderAction( get_class( $this ), $__m );
			$data['view']['params'] = $p[0];
			$data['center'] = $_r;
			$_r = $view ->display( $frame, $data );
			}
			
			$this ->hooks ->fire( 'out', array( 'type' => $type, 'method' => $__m, 'result' => &$_r ) );
		}
		elseif( is_null( $_r ) )
		{
			fb::log( $_m." not found." );
			response::__getMe() ->notfound( );
		}
		break;
		
		case 2:
		case 'block':
		
		$m = $__m.'Block';
		if( method_exists( $this, $m ) )
		{
			ob_start( );
			$_r = call_user_func_array( array( $this, $m ), $p );
			$ob = ob_get_contents(); ob_end_clean();
			
			if( !is_string( $_r ) )
			{
				if( is_array( $_r ) )
				{
					if( isset( $_r['template'] ) ) $template = $_r['template'];
					if( isset( $_r['templatens'] ) ) $templatens = $_r['templatens'];
					$_r['__url'] = $this ->getCurrentUrl( );
				}
				// if( strtolower(substr( $__c, -5 )) == 'admin' ) { $__c = 'admin/'.substr( $__c, 0 ,-5 ); }
				$_r = $view ->display( array(
					str_replace('_','/',$templatens).'/_'.$template,
					str_replace('_','/',$templatens).'/'.$template,
					$templatens.'@_'.$template,
					$templatens.'@'.$template,
					'_'.$template,
					$template 
				), $_r );
			}
			
			$_r = $ob.$_r;

			$this ->hooks ->fire( 'display', array( 'type' => $type, 'method' => $__m, 'result' => &$_r ) );
		}
		break;
		
		default:
		$_r = "<!-- unknown type of {$__c}::{$_m} block -->";
		}
		
		return $_r;
	}
	
	public function getCrumbs( )
	{
		return $this ->crumbs;
	}
	
	public function setCrumbsFrom( controller $c )
	{
		fb::log( 'copied crumbs:', $c ->getCrumbs( ) );
		$this ->crumbs = $c ->getCrumbs( );
	}
	
	public function getCurrentUrl( )
	{
		$z = $this ->getCrumbs( );
		end( $z ); $z = key( $z );
		//fb::log( $z, 'z' );
		return $z != '/' ? $z : trim( Request::__getMe( ) ->getUrlPath( ), '/' );
	}
	
	public function getInnerUrl( )
	{
		// fixme:
		$r = trim( str_replace( array( 'ajax/', 'admin/' ), '', Request::__getMe( ) ->getUrl( ) ), '/' );
		return substr( $r, strpos( $r, '/' ) );
	}
	
	public function crumbsBlock( $p, $o )
	{
		$delim = isset( $p['delimiter'] ) && strlen( $p['delimiter'] ) ? $p['delimiter'] : ' / ';
		$m = array_merge( array_fill_keys( array( 'noroot', 'nocurrent' ), false ),
			(array)( isset( $p['modes'] ) ? array_fill_keys( array_filter( explode( ' ', $p['modes'] ) ), true ) : null ) );
		
		$_r = array( );
		foreach( $this ->getCrumbs( ) as $url => $text )
			if( $url != '/' || !$m['noroot'] )
				$_r[] = sprintf( strlen( $url ) ? '<a href="%s">%s</a>' : '<i>%2$s</i>', $url, $text );
		
		return join( $delim, $_r );
	}
	
	public function pageTitleBlock( )
	{
		return (string)( isset($this ->pageTitle) ? $this ->pageTitle : ( $this ->meta['title'] ? $this ->meta['title'] : $this ->getCurrentUrl() ) );
	}
	
	
	public function showIndexPage( $p, $o )
	{
		return 'Please create file index.php in ./exts/controllers/ with class indexController and method showIndexPage.';
	}
	
	public function getSitemap( ) { return null; }
	
	
	
	
	
	
	
	static public function getMenuItems( ) { return null; }
	
	public function fullmenuBlock( $p )
	{
		$cache = cache::__getMe( );
		$cache ->enable( 'fullmenu' );
		$cache_params = array(
			'roles' => $this ->visitorRoles ->__toString(),
			'p' => $p, );
		if( $cache ->is( get_class( $this ), __METHOD__, $cache_params, '1M' ) )
		{
			$r = $cache ->getv( );
			$cache ->disable( 'fullmenu' );
			return $r;
		}
		
		
		if( !isset( $p['_'] ) || !is_array( $p['_'] ) )
		{
			$p['_'] = array( );
			$p['_']['extends'] = 'controller';
			$p['_']['suffix'] = 'Controller';
			$p['_']['listcallback'] = 'getPublicControllers';
		}
		if( !isset( $p['_']['urlbase'] ) ) $p['_']['urlbase'] = '/';
		
		$_p = $p['_'];
		$mode = rtrim(_::pick( @$p['mode'], 'list' ),'s').'s'; //list, drop -> lists, drops
		
		$items = array( );
		$specials = array( );
		$listcallback = $_p['listcallback'];
		$list = (array)(self::$listcallback()); //(array)(call_user_func( 'controller::'.$_p['listcallback'] ));
		
		switch( $mode )
		{
		default:
		case 'lists':
			foreach( $list as $c )
			{
				if( !is_subclass_of( $c.$_p['suffix'], $_p['extends'] ) ) continue;
				$urls = (array)(call_user_func( $c.$_p['suffix'].'::getMenuItems' ));
				
				foreach( array( 'hide', 'regroup', 'rename', 'renamegroup', 'order' ) as $special )
				if( isset( $urls['@'.$special] ) ) { if( !isset( $specials[$special] ) ) $specials[$special] = array( ); $specials[$special] += $urls['@'.$special]; unset( $urls['@'.$special] ); }
				
				$item['order'] = (int)(isset( $item['order'] ) ? $item['order'] : 0);
				
				$items = array_merge( $items, $urls );
			}
			
			if( is_array($specials['hide']) )	foreach( $specials['hide'] as $url ) unset( $items[$url] );
			if( is_array($specials['rename']) )	foreach( $specials['rename'] as $url => $newname ) if( isset( $items[$url] ) ) $items[$url] = $newname;
			uasort( $items, create_function( '$a,$b', '$a=@$a["order"];$b=@$b["order"]; if($a==$b)return 0; return $a<$b?-1:1;' ) );
			
			break;
		case 'drops':
			foreach( $list as $c )
			{
				if( !is_subclass_of( $c.$_p['suffix'], $_p['extends'] ) ) continue;
				$urls = (array)(call_user_func( $c.$_p['suffix'].'::getMenuItems' ));
				
				foreach( array( 'hide', 'regroup', 'rename', 'renamegroup', 'order' ) as $special )
				if( isset( $urls['@'.$special] ) ) { if( !isset( $specials[$special] ) ) $specials[$special] = array( ); $specials[$special] = array_merge( $specials[$special], $urls['@'.$special] ); unset( $urls['@'.$special] ); }
				
				foreach( $urls as $url => $item )
				{
				if( is_string($item) )		$item = array( 'text' => $item );
				if( !is_array( $item ) )	continue;
				$item['order'] = (int)(isset( $item['order'] ) ? $item['order'] : 0);
				$items[_::pick(@$item['group'],$c)][$url] = $item;
				}
			}
			
			// specials
			foreach( array_keys( $items ) as $group )
			foreach( array( 'hide', 'rename', 'regroup' ) as $spKey )
			{
				if( !isset($specials[$spKey]) || !is_array($specials[$spKey]) ) continue;
				
				foreach( array_keys( $specials[$spKey] ) as $url )
				{
				$new =& $specials[$spKey][$url];
				
				switch( $spKey )
				{
				case 'hide':
					unset( $items[$group][$new] );
					break;
				case 'rename':
					if( !isset( $items[$group][$url] ) ) break;
					$items[$group][$url]['text'] = $new;
					break;
				case 'regroup':
					if( !isset( $items[$group][$url] ) ) break;
					$items[$new][$url] = $items[$group][$url];
					unset($items[$group][$url]);
					break;
				}
				}
			}
			
			// cleanup
			foreach( array( 'hide', 'rename', 'regroup' ) as $spKey )
			{
				if( !isset($specials[$spKey]) || !is_array($specials[$spKey]) ) continue;
				unset( $specials[$spKey] );
			}
			
			// rename groups with save order
			if( is_array( $specials['renamegroup'] ) )
			{
			$_items = $items; $items = array( );
			foreach( $_items as $group => $urls )
				$items[_::pick( @$specials['renamegroup'][$group], $group )] = $urls;
			unset( $_items );
			}
			
			// group preparing
			foreach( $items as $group => $urls ) {
				// clearing of empty
				if( empty( $urls ) ) { unset( $items[$group] ); continue; }
				// ordering
				uasort( $items[$group], create_function( '$a,$b', '$a=@$a["order"];$b=@$b["order"]; if($a==$b)return 0; return $a<$b?-1:1;' ) );
			}
		}

		// group order
		if( isset( $specials['order'] ) && is_array( $specials['order'] ) && !empty( $items ) )
		{
			$_items = array( );
			$order = array_unique( $specials['order'] );
			ksort( $order );
			foreach( $order as $group )
			if( isset( $items[$group] ) )
			{
				$_items[$group] = $items[$group];
				unset( $items[$group] );
			}
			$_items += $items;
			$items = $_items;
		}
		
		//fb::log( $items, 'fullmenu' );
		//fb::log( $specials, 'specials' );
		$r['mode'] = $mode;
		$r['items'] = $items;
		$r['template'] = _::pick( @$p['template'], '_fullmenu.tpl' );
		$r['urlbase'] = $p['_']['urlbase'];
		
		$cache ->setv( $r );
		$cache ->disable( 'fullmenu' );
		
		return $r;
	}



	/**
	 * 
	 */
	public function inner_url( $method = null )
	{
		$action = self::renderAction( get_class($this), $method );
		
		$url = str_replace( array( '::index', '_', '' ), array( '', '/', '/' ), $action );
		$url = '/' . trim( $url, '/' ) . '/'; // normalize
		
		return $url;

	}

}

?>